﻿// <copyright file="TimesTags.cs" company="Microsoft Corporation">
// Copyright (c) 2009 Microsoft Corporation All Rights Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-03-06</date>
// <summary>New York Times Tags Search API</summary>

namespace NYTimes
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Json;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Windows;
    using System.Windows.Media.Imaging;
    using NYTimes.Core;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// New York Times TimesTags API
    /// </summary>
    /// <remarks>See <a target="_blank" href="http://developer.nytimes.com/docs/timestags_api">this 
    /// page</a> for 
    /// more information about this API.
    /// This is available on Silverlight but not on WPF or Windows Phone 7 Series.
    /// </remarks>
    /// <example>
    /// Sample code from DemoNYTimes TimesTagsPage.xaml page that demonstrates using the 
    /// TimesTag.Service attached property on a Silverlight Toolkit 
    /// AutoCompleteBox: <c>nyt:TimesTags.Service="{StaticResource Tags}"</c>
    /// <code>
    /// <![CDATA[
    /// <UserControl x:Class="DemoNYTimes.TimesTagsPage"
    ///    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    ///    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    ///    xmlns:inputToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"  
    ///    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    ///    xmlns:nyt="clr-namespace:NYTimes;assembly=NYTimesSilverlightKit"             
    ///    xmlns:nytvc="clr-namespace:NYTimes.ValueConverters;assembly=NYTimesSilverlightKit"
    ///    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    ///    mc:Ignorable="d">
    ///    <UserControl.Resources>
    ///        <nyt:TimesTags x:Key="Tags" d:IsDataSource="true"/>
    ///        <nytvc:DictionaryWebdingConverter x:Key="DictionaryWebdingConverter"/>
    ///        <DataTemplate x:Key="TagResultTemplate">
    ///            <Grid>
    ///                <Grid.ColumnDefinitions>
    ///                    <ColumnDefinition Width="16"/>
    ///                    <ColumnDefinition Width="Auto"/>
    ///                </Grid.ColumnDefinitions>
    ///                <TextBlock VerticalAlignment="Center" FontSize="16" FontFamily="Webdings" 
    ///                     Text="{Binding Dictionary, Converter={StaticResource DictionaryWebdingConverter}}" />
    ///                <TextBlock Margin="4,0,0,0" VerticalAlignment="Center" 
    ///                     FontFamily="Times New Roman" FontSize="12" Grid.Column="1" 
    ///                     Text="{Binding Mode=OneWay, Path=Value}" 
    ///                     TextWrapping="NoWrap"/>
    ///            </Grid>
    ///        </DataTemplate>
    ///    </UserControl.Resources>
    ///    <Grid x:Name="LayoutRoot">
    ///        <inputToolkit:AutoCompleteBox x:Name="Query" 
    ///                    Width="300"                      
    ///                    VerticalAlignment="Center"
    ///                    DataContext="{StaticResource Tags}" 
    ///                    SearchMode="None" 
    ///                    IsTextCompletionEnabled="False" 
    ///                    ItemsSource="{Binding Path=Tags}"
    ///                    ItemTemplate="{StaticResource TagResultTemplate}" 
    ///                    ValueMemberBinding="{Binding Path=Value}"
    ///                    nyt:TimesTags.Service="{StaticResource Tags}"/>
    ///    </Grid>
    /// </UserControl>
    /// ]]>
    /// </code>
    /// </example>
    public class TimesTags : INotifyPropertyChanged
    {
        #region Fields

        /// <summary>
        /// AutoComplete Dependency Property
        /// </summary>
        public static readonly DependencyProperty ServiceProperty =
            DependencyProperty.RegisterAttached(
                "Service",
                typeof(TimesTags),
                typeof(TimesTags),
                new PropertyMetadata(null, OnServiceChanged));

        /// <summary>
        /// the Resource key in the Application Resources for the API Key
        /// </summary>
        private const string NYTTagsKey = "NYTTagsKey";

        /// <summary>
        /// The Base URI
        /// </summary>
        private const string BaseUri = "http://api.nytimes.com/svc/timestags/suggest";

        /// <summary>
        /// 40 QPS rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTTimesTags", 40);

        /// <summary>
        /// the query text
        /// </summary>
        private string query;

        /// <summary>
        /// the running web client
        /// </summary>
        private WebClient client;

        /// <summary>
        /// the maximum number of tags to return
        /// </summary>
        private int max;

        /// <summary>
        /// Are the results loading
        /// </summary>
        private bool isLoading;

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the TimesTags class.
        /// </summary>
        public TimesTags()
        {
            if (Application.Current.Resources.Contains(NYTTagsKey))
            {
                this.ApiKey = Application.Current.Resources[NYTTagsKey] as string;
            }
        }
        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// The property changed event handler
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the logo for the New York Times
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822", 
            Justification = "This is exposed to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var streamResource = Application.GetResourceStream(
    new Uri("NYTimesSilverlightKit;component/Images/Logo.png", UriKind.Relative));

                var image = new BitmapImage();

                image.SetSource(streamResource.Stream);

                return image;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the results are loading
        /// </summary>
        public bool IsLoading
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("IsLoading"));
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the comma-delimited list to limit the results to the specified Times 
        /// dictionaries : (Des), (Geo), (Org), (Per)
        /// </summary>
        /// <remarks>You must include parentheses around each dictionary abbreviation.</remarks>
        [Description("A comma-delimited list to limit the results to the specified Times dictionaries : (Des), (Geo), (Org), (Per)")]
        public string Filter { get; set; }

        /// <summary>
        /// Gets or sets the maximum number of results to return
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">if the value is less than 1.</exception>
        [DefaultValue(10),
         Description("The maximum number of results to return.  Default is 10.")]
        public int Max 
        { 
            get
            {
                return this.max;
            }

            set
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException("value", "The Max value must be greater than 0.");
                }

                if (this.max != value)
                {
                    this.max = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the API Key
        /// </summary>
        [Description("The Key for the TimesTag API")]
        public string ApiKey { get; set; }

        /// <summary>
        /// Gets or sets the query
        /// </summary>
        [Description("The query text")]
        public string Query 
        { 
            get
            {
                return this.query;
            }

            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                if (this.query != value)
                {
                    this.query = value.Trim();

                    this.FetchTags(this.query);
                }
            }
        }

        /// <summary>
        /// Gets the tags returned as an enumerable collection of <see cref="NYTimes.Data.Tag"/> objects
        /// </summary>
        public IEnumerable<Tag> Tags { get; private set; }

        #endregion

        #region Methods

        /// <summary>
        /// Set the AutoComplete Data source
        /// </summary>
        /// <param name="element">a Silverlight Toolkit AutoComplete box</param>
        /// <param name="value">a TimesTag object</param>
        public static void SetService(DependencyObject element, TimesTags value)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }

            element.SetValue(ServiceProperty, value);
        }

        /// <summary>
        /// Get the AutoComplete data source
        /// </summary>
        /// <param name="element">a Silverlight Toolkit AutoComplete box</param>
        /// <returns>a TimesTag object</returns>
        public static TimesTags GetService(DependencyObject element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }

            return (TimesTags)element.GetValue(ServiceProperty);
        }

        /// <summary>
        /// Make a query on the tags API
        /// </summary>
        /// <param name="queryString">the query to the tags API</param>
        public void FetchTags(string queryString)
        {
            if (this.client != null)
            {
                this.client.CancelAsync();
            }

            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return;
            }

            this.IsLoading = true;

            var uriString = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}?query={1}&api-key={2}", BaseUri, queryString, this.ApiKey);

            var builder = new StringBuilder(uriString);

            if (!string.IsNullOrEmpty(this.Filter))
            {
                builder.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "&filter={0}", this.Filter);
            }

            if (this.Max > 0)
            {
                builder.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "&max={0}", this.Max);
            }

            uriString = builder.ToString();

            var address = new Uri(uriString, UriKind.Absolute);

            System.Diagnostics.Debug.WriteLine(address);

            this.client = new WebClient();

            this.client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(this.OnTagsReturned);

            limiting.MakeWebRequest(this.client, address);
        }

        #endregion

        #region Implementation

        /// <summary>
        /// Event handler triggered when the star column changes
        /// </summary>
        /// <param name="depObject">a Silverlight Toolkit AutoComplete Text Box</param>
        /// <param name="args">the dependency property changed event arguments</param>
        /// <remarks>We do late binding here to avoid the dependency on the Silverlight Toolkit</remarks>
        private static void OnServiceChanged(DependencyObject depObject, DependencyPropertyChangedEventArgs args)
        {
            var tags = args.NewValue as TimesTags;

            if (tags == null)
            {
                return;
            }

            var type = depObject.GetType();

            tags.PropertyChanged += new PropertyChangedEventHandler(delegate(object sender, PropertyChangedEventArgs args2)
                {
                    if (args2.PropertyName == "Tags")
                    {
                        var method = type.GetMethod("PopulateComplete");

                        if (method != null)
                        {
                            method.Invoke(depObject, null);
                        }
                    }
                });

            var populating = type.GetEvent("Populating");

            if (populating != null)
            {
                var method = typeof(Populator).GetMethod("Populating");

                var handler = Delegate.CreateDelegate(populating.EventHandlerType, method);

                populating.AddEventHandler(depObject, handler);
            }
        }

        /// <summary>
        /// Get the dictionary from the tag
        /// </summary>
        /// <param name="value">the full tag</param>
        /// <returns>the dictionary acronym</returns>
        private static string GetDictionary(string value)
        {
            var openParen = value.LastIndexOf('(');

            var closeParen = value.LastIndexOf(')');

            string dictionary = value.Substring(openParen + 1, closeParen - openParen - 1);

            return dictionary;
        }

        /// <summary>
        /// Gets the tag value from the tag
        /// </summary>
        /// <param name="value">the full tag with dictionary and quotes</param>
        /// <returns>the tag value</returns>
        private static string GetValue(string value)
        {
            var openParen = value.LastIndexOf('(');

            return value.Substring(1, openParen - 1).Trim();
        }

        /// <summary>
        /// Event handler when tags are returned
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnTagsReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            this.client = null;

            if (e.Error != null)
            {
                if (!e.Cancelled)
                {
                    System.Diagnostics.Debug.WriteLine(e.Error.Message);

                    this.IsLoading = false;
                }

                return;
            }

            this.ParseTags(e.Result);
        }

        /// <summary>
        /// Parse the JSON results
        /// </summary>
        /// <param name="result">JSON results from the Tag services</param>
        private void ParseTags(string result)
        {
            using (var reader = new StringReader(result))
            {
                var response = JsonObject.Load(reader) as JsonObject;

                if (response.ContainsKey("results"))
                {
                    var results = response["results"] as JsonArray;

                    var tags = from item in results
                               select new Tag
                               {
                                   Dictionary = GetDictionary(item.ToString()),
                                   Value = GetValue(item.ToString())
                               };

                    this.Tags = tags;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("Tags"));
                    }
                }
            }

            this.IsLoading = false;
        }

        #endregion

        #region Classes
        /// <summary>
        /// Internal populator class for the Service Attached property
        /// </summary>
        private class Populator
        {
            /// <summary>
            /// Popuplate the Autocomplete box
            /// </summary>
            /// <param name="sender">a Silverlight Toolkit AutoCompleteBox</param>
            /// <param name="args">a PopluatingEventArgs object</param>
            public static void Populating(object sender, EventArgs args)
            {
                var tags = GetService(sender as DependencyObject);

                var parameter = args.GetType().GetProperty("Parameter");

                var query = parameter.GetValue(args, null) as string;

                tags.Query = query;
            }
        }
        #endregion
    }
}
